package com.siteseat.common.foundation;

import java.util.Collection;
import java.util.concurrent.TimeUnit;

import org.apache.commons.lang3.StringUtils;

import com.siteseat.common.exception.InternalException;
import com.siteseat.common.misc.SizeUnit;

/**
 *
 */
public class Rater {

  public static final String   DEFAULT_FORMAT    = ".3";

  public static final SizeUnit DEFAULT_SIZE_UNIT = SizeUnit.KB;

  public static final TimeUnit DEFAULT_TIME_UNIT = TimeUnit.SECONDS;

  private long                 lastStartTime;

  private long                 intervalByNanoseconds;

  private long                 size;

  private SizeUnit             sizeUnit;

  private long                 cycle;

  private TimeUnit             timeUnit;

  private boolean              started;

  private String               name;

  /**
   * @param sizeUnit
   */
  public Rater( SizeUnit sizeUnit, TimeUnit timeUnit ) {
    this.sizeUnit = sizeUnit;

    switch( timeUnit ) {
      case NANOSECONDS:
      case MICROSECONDS:
      case MILLISECONDS:
      case SECONDS:
      case MINUTES:
      case HOURS:
      case DAYS:
        break;
      default:
        throw new InternalException( "Unsupported time unit: " + timeUnit );
    }
    this.timeUnit = timeUnit;
  }

  /**
   * @param sizeUnit
   * @param timeUnit
   * @param name
   */
  public Rater( SizeUnit sizeUnit, TimeUnit timeUnit, String name ) {
    this( sizeUnit, timeUnit );
    setName( name );
  }

  /**
   * @param sizeUnit
   * @param timeUnit
   * @param name
   */
  public Rater( SizeUnit sizeUnit, TimeUnit timeUnit, String name, Collection<Rater> subRaters ) {
    this( sizeUnit, timeUnit, name );
    mergeWith( subRaters );
  }

  /**
   * @param that
   */
  public void mergeWith( Rater that ) {
    this.intervalByNanoseconds += that.intervalByNanoseconds;
    this.size += that.size;
    this.cycle += that.cycle;
  }

  /**
   * @param that
   */
  public void mergeWith( Collection<Rater> others ) {
    for( Rater that: others ) {
      mergeWith( that );
    }
  }

  /**
   * Gets the name
   * 
   * @return the name
   */
  public String getName() {
    return name;
  }

  /**
   * Sets the name
   * 
   * @param name
   *          the name to set
   */
  public void setName( String name ) {
    this.name = name;
  }

  /**
   * @param size
   * @param sizeUnit
   */
  public Rater( long size, SizeUnit sizeUnit, TimeUnit timeUnit ) {
    this( sizeUnit, timeUnit );
    this.size = size;
  }

  /**
   * Gets the cycle
   * 
   * @return the cycle
   */
  public long getCycle() {
    return cycle;
  }

  /**
   * Gets the timeUnit
   * 
   * @return the timeUnit
   */
  public TimeUnit getTimeUnit() {
    return timeUnit;
  }

  /**
   * Gets the sizeUnit
   * 
   * @return the sizeUnit
   */
  public SizeUnit getSizeUnit() {
    return sizeUnit;
  }

  /**
   * Gets the lastStartTime
   * 
   * @return the lastStartTime
   */
  public long getLastStartTime() {
    return lastStartTime;
  }

  /**
   * @return
   */
  public static long now() {
    return System.nanoTime();
  }

  /**
   */
  public void start() {
    if( this.started ) {
      stop();
    }
    this.lastStartTime = now();
    this.started = true;
  }

  /**
   */
  public void stop() {
    if( this.started ) {
      this.intervalByNanoseconds += now() - getLastStartTime();
      this.cycle++;
      this.started = false;
    }
  }

  /**
   * @return
   */
  public boolean started() {
    return this.started;
  }

  /**
   * @return
   */
  public long getIntervalByNanoseconds() {
    return intervalByNanoseconds;
  }

  /**
   * @param tu
   * @return
   */
  public double getInterval() {
    double intervalByTimeUnit = getIntervalByNanoseconds();

    switch( getTimeUnit() ) {
      case NANOSECONDS:
        return intervalByTimeUnit;
      case MICROSECONDS:
        return intervalByTimeUnit / ( 1000 );
      case MILLISECONDS:
        return intervalByTimeUnit / ( 1000 * 1000 );
      case SECONDS:
        return intervalByTimeUnit / ( 1000 * 1000 * 1000 );
      case MINUTES:
        return intervalByTimeUnit / ( 1000 * 1000 * 1000 * 60 );
      case HOURS:
        return intervalByTimeUnit / ( 1000 * 1000 * 1000 * 3600 );
      case DAYS:
        return intervalByTimeUnit / ( 1000 * 1000 * 1000 * 3600 * 24 );
      default:
        throw new InternalException( "Unsupported time unit: " + getTimeUnit() );
    }
  }

  /**
   * @param su
   * @param tu
   * @return
   */
  public String getIntervalText( String intervalFormat ) {
    double r = getInterval();
    r = Double.isNaN( r ) ? 0.0f : r;

    StringBuilder fullFormat = new StringBuilder();
    fullFormat.append( '%' ).append( intervalFormat ).append( "f " );
    fullFormat.append( getTimeUnit() );
    return String.format( fullFormat.toString(), Double.valueOf( r ) );
  }

  /**
   * @param su
   * @param tu
   * @return
   */
  public String getIntervalText() {
    return getIntervalText( DEFAULT_FORMAT );
  }

  /**
   * @param su
   * @param tu
   * @return
   */
  public double getRate() {
    return getSize() / getInterval();
  }

  /**
   * @param su
   * @param tu
   * @return
   */
  public String getRateText( String rateFormat ) {
    double r = getRate();
    r = Double.isNaN( r ) ? 0.0f : r;

    StringBuilder fullFormat = new StringBuilder();
    fullFormat.append( '%' ).append( rateFormat ).append( "f " );
    fullFormat.append( getSizeUnit() ).append( '/' ).append( getTimeUnit() );
    return String.format( fullFormat.toString(), Double.valueOf( r ) );
  }

  /**
   * @return
   */
  public String getRateText() {
    return getRateText( DEFAULT_FORMAT );
  }

  /**
   * {@inheritDoc}
   */
  @Override
  public String toString() {
    return getRateText();
  }

  /**
   * @param header
   * @return
   */
  protected String reportTitle( String header, boolean includeName ) {
    StringBuilder buf = new StringBuilder();

    boolean nameIsNotBlank = includeName && StringUtils.isNotBlank( getName() );
    boolean headerIsNotBlank = StringUtils.isNotBlank( header );

    if( nameIsNotBlank ) {
      if( headerIsNotBlank ) {
        buf.append( getName() ).append( " - " );
      } else {
        buf.append( getName() ).append( ": " );
      }
    }
    if( headerIsNotBlank ) {
      buf.append( header ).append( ": " );
    }

    return buf.toString();
  }

  /**
   * @param header
   * @return
   */
  public String reportRate( String header ) {
    StringBuilder buf = new StringBuilder();

    buf.append( reportTitle( header, false ) );
    buf.append( getSizeText() ).append( " in " ).append( getIntervalText() );
    buf.append( ' ' );
    buf.append( '(' ).append( getCycle() ).append( " cycles, " ).append( getRateText() ).append( ')' );

    return buf.toString();
  }

  /**
   * @param header
   * @return
   */
  public String reportInterval( String header ) {
    StringBuilder buf = new StringBuilder();

    buf.append( reportTitle( header, false ) );
    buf.append( getIntervalText() );

    return buf.toString();
  }

  /**
   * @param header
   * @return
   */
  public String reportSize( String header ) {
    StringBuilder buf = new StringBuilder();

    buf.append( reportTitle( header, false ) );
    buf.append( getSizeText() );

    return buf.toString();
  }

  /**
   * Gets the size
   * 
   * @return the size
   */
  public long getSizeByBytes() {
    return size;
  }

  /**
   * @return
   */
  public double getSize() {
    return getSizeUnit().convertFrom( getSizeByBytes(), SizeUnit.BYTE );
  }

  /**
   * @param su
   * @param tu
   * @return
   */
  public String getSizeText( String sizeFormat ) {
    double r = getSize();
    r = Double.isNaN( r ) ? 0.0f : r;

    StringBuilder fullFormat = new StringBuilder();
    fullFormat.append( '%' ).append( sizeFormat ).append( "f " );
    fullFormat.append( getSizeUnit() );
    return String.format( fullFormat.toString(), Double.valueOf( r ) );
  }

  /**
   * @param su
   * @param tu
   * @return
   */
  public String getSizeText() {
    return getSizeText( DEFAULT_FORMAT );
  }

  /**
   * @param delta
   */
  public void increase( int delta ) {
    this.size += delta;
  }

  /**
   * 
   *
   */
  public void increaseByOne() {
    increase( 1 );
  }

  /**
   * @param delta
   */
  public void decrease( int delta ) {
    this.size -= delta;
  }

  /**
   * 
   *
   */
  public void decreaseByOne() {
    decrease( 1 );
  }

  /**
   * 
   */
  public String reportAll( String header ) {
    StringBuilder r = new StringBuilder();

    r.append( reportTitle( header, true ) ).append( '\n' );
    r.append( reportRate( "I/O busy thoughput" ) ).append( '\n' );
    r.append( reportInterval( "Time" ) ).append( '\n' );
    r.append( reportSize( "Size" ) );

    return r.toString();
  }

  /**
   * 
   */
  public static void report( ObjectLogger log, Collection<Rater> raters, String header, String totalName,
          SizeUnit sizeUnit, TimeUnit timeUnit ) {
    Rater total = new Rater( sizeUnit, timeUnit, totalName, raters );
    log.info( total.reportAll( header ) );

    for( Rater r: raters ) {
      log.info( r.reportAll( header ) );
    }
  }

  /**
   * @param args
   * @throws Exception
   */
  public static void main2( String[] args ) throws Exception {
    Rater r = new Rater( SizeUnit.MB, TimeUnit.SECONDS );
    r.setName( "test" );
    r.start();
    Thread.sleep( 500 );
    r.increase( 1024 * 1024 );
    r.stop();

    r.start();
    r.increase( 768 * 1024 );
    Thread.sleep( 200 );
    r.stop();

    System.out.println( r.reportAll( "\n" ) );
  }

  /**
   * @param args
   * @throws Exception
   */
  public static void main( String[] args ) throws Exception {
    Rater r = new Rater( SizeUnit.MB, TimeUnit.SECONDS );
    r.setName( "test" );
    r.start();
    Thread.sleep( 500 );
    r.increase( 1024 * 1024 );
    r.stop();

    r.start();
    r.increase( 768 * 1024 );
    Thread.sleep( 200 );
    r.stop();

    System.out.println( r.reportAll( "\n" ) );
  }

}
